Skip to content

fix(chat): GenUI streaming architecture — composition-level orchestration#246

Merged
blove merged 6 commits into
mainfrom
claude/genui-architecture-fix
May 11, 2026
Merged

fix(chat): GenUI streaming architecture — composition-level orchestration#246
blove merged 6 commits into
mainfrom
claude/genui-architecture-fix

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 11, 2026

Summary

Replaces PR #245's chat-message-level GenUI suppression — which broke the actual surface rendering by hiding chat-message's entire ng-content slot — with a clean separation of concerns:

  • Classifier (`createContentClassifier`) stays content-only. Renamed `'undetermined'` → `'pending'`, plus a patience fix: when content starts with `-`, the classifier waits until either the full `---a2ui_JSON---` prefix arrives (→ `'a2ui'`) or enough chars rule it out (→ `'markdown'`). Previously committed to markdown on the first `-`, missing every streaming A2UI payload.

  • Chat composition owns "is this a GenUI turn?" via a new `isGenuiTurn(message, prevMessage)` method that reads message structure (`tool_calls` field, `content[].type === 'function_call'` block, previous-tool-message name).

  • `` new primitive — card-shaped placeholder, now rendered from the composition template as a sibling of `` / `` — not a wrapper around them. Skeleton shows when classifier is `'pending'` AND `isGenuiTurn` is true, OR when classifier is `'a2ui'` but no surfaces have parsed yet (late-arrival guard).

  • `` gains an `excludeToolNames` input. Composition passes the GenUI tool names so internal dispatchers don't render args JSON as cards.

  • chat-message untouched — was already in the clean state since this branch forked from origin/main before PR feat(chat): suppress streaming JSON for A2UI tool calls #245.

Spec: `docs/superpowers/specs/2026-05-11-genui-streaming-architecture-design.md`.

What this fixes

The user flow today: typing indicator → flash of JSON → late "Building UI" skeleton → never resolves to the rendered surface.

After this PR: typing indicator → skeleton from the first token → rendered surface mounts as soon as envelopes parse.

Test plan

  • `nx test chat` — all green (existing + classifier patience tests + chat-tool-calls filter tests + isGenuiTurn tests)
  • `nx build chat` + `nx lint chat` green
  • Live smoke at `/embed` with the "Render a settings card…" suggestion — verify clean skeleton → surface transition with no JSON visible
  • Live smoke with a non-GenUI prompt — verify normal markdown streaming, no skeleton flash
  • CI green

Closes #245.

🤖 Generated with Claude Code

blove added 6 commits May 11, 2026 12:33
Card-shaped placeholder rendered in place of streaming
tool-call JSON while an A2UI / json-render surface is being
built. Three shimmer rows + 'Building UI…' status label.
Themed via existing chat-tokens (separator color, surface-alt
background) so it inherits theme overrides.
Two changes:

1. Rename ContentType value 'undetermined' to 'pending'. The new
   name better reflects what the state means (we're waiting for
   enough content to decide), and is the state the chat composition
   reads when deciding whether to show the GenUI skeleton.

2. Add patience for the A2UI prefix. When the first non-whitespace
   char is '-', stay 'pending' until either the full prefix matches
   (commit to 'a2ui') or enough chars arrive without matching
   (commit to 'markdown').
New optional input that filters out tool groups whose name is in
the exclude set. Used by chat compositions to hide
orchestration-only tool calls (e.g. GenUI dispatchers) whose
streaming args are not meaningful to the user.
Adds isGenuiTurn(message, prevMessage) to the chat composition,
which inspects message structure across three independent signals
(tool_calls field, function_call content block, prev-tool-message
name) to decide whether this assistant turn is producing a GenUI
surface.

Template changes inside the AI message branch:
- <chat-tool-calls> now gets [excludeToolNames]="genuiToolNames()",
  so internal GenUI dispatchers don't render args JSON as cards.
- New <chat-genui-skeleton /> branch renders when the classifier
  is 'pending' (or 'a2ui' with no surfaces yet) AND isGenuiTurn
  is true — bridging the gap between streaming start and the
  rendered surface mounting.

The skeleton is a SIBLING of <a2ui-surface> and <chat-generative-ui>,
not a wrapper around them — so it cleanly hands off once content
disambiguates.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 11, 2026 7:41pm

Request Review

@blove blove merged commit f43df73 into main May 11, 2026
14 checks passed
blove added a commit that referenced this pull request May 11, 2026
)

Live smoke against PR #246's architecture revealed the classifier
patience fix isn't enough on its own: during the sub-LLM phase of
a GenUI run, raw JSON envelopes stream INTO the assistant
message's content BEFORE emit_generated_surface wraps them with
the A2UI prefix. The first chunks arrive as '[' (JSON array open)
which the classifier locks in as 'markdown' — and the patience
fix only protects the '-' first-char ambiguity.

Fix: on a GenUI turn, suppress chat-streaming-md unconditionally
until the classifier resolves to 'a2ui' or 'json-render'.
Branches restructured as @else if so they're mutually exclusive,
preventing any flash of streaming JSON.

Once emit_generated_surface's wrapped payload arrives and the
classifier's reset-on-shrink path re-classifies to 'a2ui', the
skeleton hands off to the rendered surface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant